/*
 ********************************************************************************
 *
 * Copyright (c) 2019-2024 Renesas Electronics Corporation and/or its affiliates
 *
 ********************************************************************************
 */

package com.dialog.suotalib.suota;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.util.Log;

import com.greyshark.lib.bluetooth.R;
import com.dialog.suotalib.dialogs.DialogFragmentBuilder;
import com.dialog.suotalib.dialogs.DialogFragmentManager;
import com.dialog.suotalib.dialogs.SupportCustomDialogFragment;
import com.dialog.suotalib.global.SuotaLibConfig;
import com.dialog.suotalib.global.SuotaLibConfig.Default;
import com.dialog.suotalib.global.SuotaLibConfig.PREF;
import com.dialog.suotalib.global.SuotaLibLog;
import com.dialog.suotalib.global.SuotaProfile;
import com.dialog.suotalib.global.SuotaProfile.ScanStatus;
import com.dialog.suotalib.interfaces.callbacks.ISuotaScanner;
import com.dialog.suotalib.scan.SuotaScanner;
import com.dialog.suotalib.utils.MainThread;
import com.dialog.suotalib.utils.RuntimePermissionChecker;
import com.dialog.suotalib.utils.SuotaUtil;

import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

import static com.dialog.suotalib.global.SuotaProfile.Constants.MEMORY_TYPE_EXTERNAL_I2C;
import static com.dialog.suotalib.global.SuotaProfile.Constants.MEMORY_TYPE_EXTERNAL_SPI;
import static com.dialog.suotalib.global.SuotaProfile.Constants.SUOTA_ASYMMETRIC_REBOOT;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_FIRMWARE_REVISION_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_HARDWARE_REVISION_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_IEEE_11073;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_MANUFACTURER_NAME_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_MODEL_NUMBER_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_PNP_ID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_SERIAL_NUMBER_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_SOFTWARE_REVISION_STRING;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CHARACTERISTIC_SYSTEM_ID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.CLIENT_CONFIG_DESCRIPTOR;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SERVICE_DEVICE_INFORMATION;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_ASYMMETRIC_CHARACTERISTIC_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_ASYMMETRIC_DATA_CHARACTERISTIC_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_ASYMMETRIC_SERVICE_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_GPIO_MAP_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_L2CAP_PSM_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_MEM_DEV_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_MEM_INFO_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_MTU_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_PATCH_DATA_CHAR_SIZE_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_PATCH_DATA_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_PATCH_LEN_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_SERVICE_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_SERV_STATUS_UUID;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_VERSION_UUID;

public class SuotaManager implements LifecycleObserver {
    private static final String TAG = "SuotaManager";

    public enum ManagerState {
        DISCONNECTED,
        CONNECTING,
        CONNECTED
    }

    private Context context;
    private BluetoothDevice device;
    private SuotaManagerCallback suotaManagerCallback;
    private BluetoothGatt gatt;
    private GattCallback gattCallback;
    private ManagerState state;
    private SuotaProtocol suotaProtocol;
    private WeakReference<Context> uiContextWeakReference;
    private SupportCustomDialogFragment rebootDialog;

    private LinkedList<GattOperation> gattQueue = new LinkedList<>();
    private boolean gattOperationPending;
    private final Object gattQueueLock = new Object();

    private boolean refreshOnConnectionCalled;
    private boolean refreshAfterUpdatePending;
    private boolean rebootSent;
    private boolean requestConnectionPriority = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SuotaLibConfig.REQUEST_CONNECTION_PRIORITY;

    // SUOTA configuration
    private SuotaFile suotaFile;
    private int blockSize = Default.BLOCK_SIZE;
    private int chunkSize = Default.CHUNK_SIZE;
    private int imageBank = Default.IMAGE_BANK;
    private int memoryType = Default.MEMORY_TYPE;
    // SPI
    private int misoGpio = Default.MISO_GPIO;
    private int mosiGpio = Default.MOSI_GPIO;
    private int csGpio = Default.CS_GPIO;
    private int sckGpio = Default.SCK_GPIO;
    // I2C
    private int i2cDeviceAddress = Integer.decode(Default.I2C_DEVICE_ADDRESS);
    private int sclGpio = Default.SCL_GPIO;
    private int sdaGpio = Default.SDA_GPIO;

    // region Characteristics references, values and have read flags
    // Suota Support
    private BluetoothGattService suotaService;
    private BluetoothGattCharacteristic memDevCharacteristic;
    private BluetoothGattCharacteristic gpioMapCharacteristic;
    private BluetoothGattCharacteristic memoryInfoCharacteristic;
    private BluetoothGattCharacteristic patchLengthCharacteristic;
    private BluetoothGattCharacteristic patchDataCharacteristic;
    private BluetoothGattCharacteristic serviceStatusCharacteristic;
    private BluetoothGattDescriptor serviceStatusClientConfigDescriptor;

    // Suota Info
    private BluetoothGattCharacteristic suotaVersionCharacteristic;
    private BluetoothGattCharacteristic patchDataSizeCharacteristic;
    private BluetoothGattCharacteristic mtuCharacteristic;
    private BluetoothGattCharacteristic l2capPsmCharacteristic;

    private int suotaVersion;
    private int mtu = SuotaProfile.DEFAULT_MTU;
    private int patchDataSize = Default.CHUNK_SIZE;
    private int l2capPsm;
    private boolean readSuotaVersion;
    private boolean readPatchDataSize;
    private boolean readMtu;
    private boolean readL2capPsm;

    // Asymmetric Suota
    private BluetoothGattService asymmetricService;
    private BluetoothGattCharacteristic asymmetricCharacteristic;
    private BluetoothGattCharacteristic asymmetricDataCharacteristic;
    private boolean asymmetricSupport;
    private boolean asymmetricPending;
    private byte[] asymmetricData;
    private SuotaScanner asymmetricScanner;

    // Device Info
    private BluetoothGattService deviceInfoService;
    private BluetoothGattCharacteristic manufacturerNameCharacteristic;
    private BluetoothGattCharacteristic modelNumberCharacteristic;
    private BluetoothGattCharacteristic serialNumberCharacteristic;
    private BluetoothGattCharacteristic hardwareRevisionCharacteristic;
    private BluetoothGattCharacteristic firmwareRevisionCharacteristic;
    private BluetoothGattCharacteristic softwareRevisionCharacteristic;
    private BluetoothGattCharacteristic systemIdCharacteristic;
    private BluetoothGattCharacteristic ieee11073Characteristic;
    private BluetoothGattCharacteristic pnpIdCharacteristic;

    private String manufacturer;
    private String modelNumber;
    private String serialNumber;
    private String hardwareRevision;
    private String firmwareRevision;
    private String softwareRevision;
    private byte[] systemId;
    private byte[] ieee11073;
    private byte[] pnpId;

    // endregion

    private HashMap<UUID, BluetoothGattCharacteristic> suotaInfoMap;
    private HashMap<UUID, BluetoothGattCharacteristic> deviceInfoMap;
    private final Object suotaInfoMapLock = new Object();
    private final Object deviceInfoMapLock = new Object();
    private boolean isSuotaInfoReadGroupPending;
    private AtomicBoolean isDeviceInfoReadGroupPending;
    private int totalSuotaInfo;
    private int totalDeviceInfo;

    private static final List<UUID> suotaInfoUuids = Arrays.asList(
            SUOTA_VERSION_UUID,
            SUOTA_PATCH_DATA_CHAR_SIZE_UUID,
            SUOTA_MTU_UUID,
            SUOTA_L2CAP_PSM_UUID);

    private static final List<UUID> deviceInfoUuids = Arrays.asList(
            CHARACTERISTIC_MANUFACTURER_NAME_STRING,
            CHARACTERISTIC_MODEL_NUMBER_STRING,
            CHARACTERISTIC_SERIAL_NUMBER_STRING,
            CHARACTERISTIC_HARDWARE_REVISION_STRING,
            CHARACTERISTIC_FIRMWARE_REVISION_STRING,
            CHARACTERISTIC_SOFTWARE_REVISION_STRING,
            CHARACTERISTIC_SYSTEM_ID,
            CHARACTERISTIC_IEEE_11073,
            CHARACTERISTIC_PNP_ID);

    // region Suota Manager API

    public SuotaManager(Context context, BluetoothDevice device, SuotaManagerCallback suotaManagerCallback) {
        this.context = context.getApplicationContext();
        this.device = device;
        this.suotaManagerCallback = suotaManagerCallback;
        state = ManagerState.DISCONNECTED;
        reset();
    }

    public SuotaManager(Lifecycle lifecycle, Context context, BluetoothDevice device, SuotaManagerCallback suotaManagerCallback) {
        this(context, device, suotaManagerCallback);
        registerLifecycle(lifecycle);
    }

    private void registerLifecycle(Lifecycle lifecycle) {
        lifecycle.addObserver(this);
    }

    public static boolean checkPermissions(Context context, RuntimePermissionChecker permissionChecker, RuntimePermissionChecker.PermissionRequestCallback callback) {
        if (Build.VERSION.SDK_INT < 23)
            return true;
        String[] permissions = new String[0];
        int rationale = 0;
        if (!SuotaLibConfig.SCOPED_STORAGE) {
            permissions = new String[] { Manifest.permission.READ_EXTERNAL_STORAGE };
            rationale = R.string.suota_storage_permission_rationale;
        }
        if (!SuotaUtil.checkConnectPermission(context)) {
            permissions = Arrays.copyOf(permissions, permissions.length + 1);
            permissions[permissions.length - 1] = Manifest.permission.BLUETOOTH_CONNECT;
        }
        return permissions.length == 0 || permissionChecker.checkPermissions(permissions, rationale != 0 ? context.getString(rationale) : null, callback);
    }

    private static Uri FILE_PATH_URI;
    private static DocumentFile FILE_PATH;

    public static boolean checkStorage(Context context) {
        if (!SuotaLibConfig.SCOPED_STORAGE)
            return true;
        if (isFilePathValid())
            return true;
        if (FILE_PATH_URI == null) {
            SharedPreferences preferences = context.getSharedPreferences(PREF.NAME, Context.MODE_PRIVATE);
            String uri = preferences.getString(PREF.filePathUri, null);
            if (uri != null)
                FILE_PATH_URI = Uri.parse(uri);
        }
        if (FILE_PATH_URI != null)
            FILE_PATH = DocumentFile.fromTreeUri(context.getApplicationContext(), FILE_PATH_URI);
        return isFilePathValid();
    }

    public static Uri getFilePathUri() {
        return FILE_PATH_URI;
    }

    public static void setFilePathUri(Context context, Uri filePathUri) {
        if (FILE_PATH_URI != null) {
            try {
                context.getContentResolver().releasePersistableUriPermission(FILE_PATH_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            } catch (Exception e) {
                Log.e(TAG, "Failed to release URI permission: " + FILE_PATH_URI);
            }
        }
        FILE_PATH_URI = filePathUri;
        context.getSharedPreferences(PREF.NAME, Context.MODE_PRIVATE).edit().putString(PREF.filePathUri, FILE_PATH_URI.toString()).apply();
        FILE_PATH = DocumentFile.fromTreeUri(context.getApplicationContext(), FILE_PATH_URI);
        try {
            context.getContentResolver().takePersistableUriPermission(FILE_PATH_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        } catch (Exception e) {
            Log.e(TAG, "Failed to persist URI permission: " + FILE_PATH_URI);
        }
    }

    public static boolean isFilePathValid() {
        return FILE_PATH != null && FILE_PATH.exists() && FILE_PATH.isDirectory() && FILE_PATH.canRead() && FILE_PATH.canWrite();
    }

    public static DocumentFile getFilePath() {
        return FILE_PATH;
    }

    // region Public Getters and Setters

    public ManagerState getState() {
        return state;
    }

    /**
     * Setter for the firmware is going to be used during the update
     *
     * @param suotaFile firmware file
     */
    public void setSuotaFile(SuotaFile suotaFile) {
        this.suotaFile = suotaFile;
        suotaFile.setBlockSize(blockSize);
        suotaFile.setChunkSize(chunkSize);
        suotaFile.load(context);

        if (!suotaFile.isLoaded()) {
            notifyFailure(SuotaProfile.Errors.FIRMWARE_LOAD_FAILED);
        }
        if (SuotaLibConfig.CHECK_HEADER_CRC) {
            if (suotaFile.hasHeaderInfo() && !suotaFile.isHeaderCrcValid()) {
                Log.e(TAG, "Firmware CRC validation failed");
                notifyFailure(SuotaProfile.Errors.INVALID_FIRMWARE_CRC);
            }
        }
    }

    /**
     * Returns the firmware file used during the update
     *
     * @return firmware file
     */
    public SuotaFile getSuotaFile() {
        return suotaFile;
    }

    public int getBlockSize() {
        return blockSize;
    }

    public void setBlockSize(int blockSize) {
        this.blockSize = blockSize;
    }

    public int getChunkSize() {
        return chunkSize;
    }

    public void setChunkSize(int chunkSize) {
        this.chunkSize = chunkSize;
    }

    public int getImageBank() {
        return imageBank;
    }

    public void setImageBank(int imageBank) {
        this.imageBank = imageBank;
    }

    public int getMemoryType() {
        return memoryType;
    }

    public void setMemoryType(int memoryType) {
        this.memoryType = memoryType;
    }

    public int getMisoGpio() {
        return misoGpio;
    }

    public void setMisoGpio(int misoGpio) {
        this.misoGpio = misoGpio;
    }

    public int getMosiGpio() {
        return mosiGpio;
    }

    public void setMosiGpio(int mosiGpio) {
        this.mosiGpio = mosiGpio;
    }

    public int getCsGpio() {
        return csGpio;
    }

    public void setCsGpio(int csGpio) {
        this.csGpio = csGpio;
    }

    public int getSckGpio() {
        return sckGpio;
    }

    public void setSckGpio(int sckGpio) {
        this.sckGpio = sckGpio;
    }

    public int getI2cDeviceAddress() {
        return i2cDeviceAddress;
    }

    public void setI2cDeviceAddress(int i2cDeviceAddress) {
        this.i2cDeviceAddress = i2cDeviceAddress;
    }

    public int getSclGpio() {
        return sclGpio;
    }

    public void setSclGpio(int sclGpio) {
        this.sclGpio = sclGpio;
    }

    public int getSdaGpio() {
        return sdaGpio;
    }

    public void setSdaGpio(int sdaGpio) {
        this.sdaGpio = sdaGpio;
    }

    public void setRequestConnectionPriority(boolean requestConnectionPriority) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
            this.requestConnectionPriority = requestConnectionPriority;
    }

    public boolean getRequestConnectionPriority() {
        return requestConnectionPriority;
    }

    /**
     * Returns the connected Bluetooth Device
     *
     * @return bluetooth device
     */
    public BluetoothDevice getDevice() {
        return device;
    }

    /**
     * Returns the name of the connected Bluetooth Device
     *
     * @return name of bluetooth device
     */
    public String getDeviceName() {
        return device != null ? SuotaUtil.getName(device) : null;
    }

    public BluetoothGattService getSuotaService() {
        return suotaService;
    }

    public BluetoothGattCharacteristic getMemDevCharacteristic() {
        return memDevCharacteristic;
    }

    public BluetoothGattCharacteristic getGpioMapCharacteristic() {
        return gpioMapCharacteristic;
    }

    public BluetoothGattCharacteristic getMemoryInfoCharacteristic() {
        return memoryInfoCharacteristic;
    }

    public BluetoothGattCharacteristic getPatchLengthCharacteristic() {
        return patchLengthCharacteristic;
    }

    public BluetoothGattCharacteristic getPatchDataCharacteristic() {
        return patchDataCharacteristic;
    }

    public BluetoothGattCharacteristic getServiceStatusCharacteristic() {
        return serviceStatusCharacteristic;
    }

    public BluetoothGattDescriptor getServiceStatusClientConfigDescriptor() {
        return serviceStatusClientConfigDescriptor;
    }

    public BluetoothGattCharacteristic getSuotaVersionCharacteristic() {
        return suotaVersionCharacteristic;
    }

    public BluetoothGattCharacteristic getPatchDataSizeCharacteristic() {
        return patchDataSizeCharacteristic;
    }

    public BluetoothGattCharacteristic getMtuCharacteristic() {
        return mtuCharacteristic;
    }

    public BluetoothGattCharacteristic getL2capPsmCharacteristic() {
        return l2capPsmCharacteristic;
    }

    public double getAvg() {
        return suotaProtocol != null ? suotaProtocol.getAvg() : -1;
    }

    public double getMax() {
        return suotaProtocol != null ? suotaProtocol.getMax() : -1;
    }

    public double getMin() {
        return suotaProtocol != null ? suotaProtocol.getMin() : -1;
    }

    // endregion

    // region READ CHAR: HashMap getters

    /**
     * Returns a HashMap of the device's info read
     * HashMap's key is the corresponding UUID and value is the characteristic read itself
     *
     * @return the HashMap of the device's info if they have been read,
     * otherwise null
     */
    public HashMap<UUID, BluetoothGattCharacteristic> getDeviceInfoMap() {
        return deviceInfoMap;
    }

    /**
     * Returns a HashMap of the suota info read
     * HashMap's key is the corresponding UUID and value is the characteristic read itself
     *
     * @return the HashMap of the suota info if they have been read,
     * otherwise null
     */
    public HashMap<UUID, BluetoothGattCharacteristic> getSuotaInfoMap() {
        return suotaInfoMap;
    }

    /**
     * Returns a HashMap of the device's info read
     * HashMap's key is the corresponding UUID and value is the characteristic's value parse to string
     *
     * @return the HashMap of the device's info if they have been read,
     * otherwise an empty HashMap
     */
    public HashMap<UUID, String> getFormattedDeviceInfoMap() {
        synchronized (deviceInfoMapLock) {
            HashMap<UUID, String> formattedDeviceInfoMap = new HashMap<>();
            for(Map.Entry<UUID, BluetoothGattCharacteristic> entry : deviceInfoMap.entrySet()) {
                formattedDeviceInfoMap.put(entry.getKey(), new String(entry.getValue().getValue(), StandardCharsets.UTF_8));
            }
            return formattedDeviceInfoMap;
        }
    }

    /**
     * Returns a HashMap of the suota info read
     * HashMap's key is the corresponding UUID and value is the characteristic's value
     *
     * @return the HashMap of the suota info if they have been read,
     * otherwise an empty HashMap
     */
    public HashMap<UUID, Integer> getFormattedSuotaInfoMap() {
        synchronized (suotaInfoMapLock) {
            HashMap<UUID, Integer> formattedSuotaInfoMap = new HashMap<>();
            for(Map.Entry<UUID, BluetoothGattCharacteristic> entry : suotaInfoMap.entrySet()) {
                Integer info = null;
                byte[] value = entry.getValue().getValue();
                if (entry.getKey().equals(SUOTA_VERSION_UUID)) {
                    if (value.length >= 1)
                        info = value[0] & 0xff;
                } else {
                    if (value.length >= 2)
                        info = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
                }
                formattedSuotaInfoMap.put(entry.getKey(), info);
            }
            return formattedSuotaInfoMap;
        }
    }

    // endregion

    // region READ CHAR: Getters for last read value of specific Characteristic

    /**
     * Gets the last read value of manufacturer name characteristic
     * @return last read value, null if haven't read yet
     */
    public String getManufacturerName() {
        return manufacturer;
    }

    /**
     * Gets the last read value of model number characteristic
     * @return last read value, null if haven't read yet
     */
    public String getModelNumber() {
        return modelNumber;
    }

    /**
     * Gets the last read value of serial number characteristic
     * @return last read value, null if haven't read yet
     */
    public String getSerialNumber() {
        return serialNumber;
    }

    /**
     * Gets the last read value of hardware revision characteristic
     * @return last read value, null if haven't read yet
     */
    public String getHardwareRevision() {
        return hardwareRevision;
    }

    /**
     * Gets the last read value of firmware revision characteristic
     * @return last read value, null if haven't read yet
     */
    public String getFirmwareRevision() {
        return firmwareRevision;
    }

    /**
     * Gets the last read value of software revision characteristic
     * @return last read value, null if haven't read yet
     */
    public String getSoftwareRevision() {
        return softwareRevision;
    }

    /**
     * Gets the last read value of suota version characteristic
     * @return last read value, 0 if haven't read yet
     */
    public int getSuotaVersion() {
        return suotaVersion;
    }

    /**
     * Gets the last read value of patch date size characteristic
     * @return last read value, default value (20) if haven't read yet
     */
    public int getPatchDataSize() {
        return patchDataSize;
    }

    /**
     * Gets the last read value of mtu characteristic
     * @return last read value, default value (23) if haven't read yet
     */
    public int getMtu() {
        return mtu;
    }

    /**
     * Gets the last read value of l2cap characteristic
     * @return last read value, 0 if haven't read yet
     */
    public int getL2capPsm() {
        return l2capPsm;
    }

    public byte[] getSystemId() {
        return systemId;
    }

    public byte[] getIeee11073() {
        return ieee11073;
    }

    public byte[] getPnpId() {
        return pnpId;
    }

    // endregion

    // region READ CHAR: Flags indicating if specific suota info has been read

    /**
     * Checks if the suota version characteristic has been read
     * @return true if it has, otherwise false
     */
    public boolean haveReadSuotaVersion() {
        return readSuotaVersion;
    }

    /**
     * Checks if the patch data size characteristic has been read
     * @return true if it has, otherwise false
     */
    public boolean haveReadPatchDataSize() {
        return readPatchDataSize;
    }

    /**
     * Checks if the mtu characteristic has been read
     * @return true if it has, otherwise false
     */
    public boolean haveReadMtu() {
        return readMtu;
    }

    /**
     * Checks if the l2cap characteristic has been read
     * @return true if it has, otherwise false
     */
    public boolean haveReadL2capPsm() {
        return readL2capPsm;
    }

    // endregion

    // region READ CHAR: Read specific characteristic

    /**
     * Read the characteristic with the given UUID.
     * Acceptable UUIDS are only Device's info UUID or Suota info UUID found on SuotaProfile class
     * @param uuid the UUID of the characteristic to be read
     * @throws Exception in case there is no connection with BLE device
     */
    public void readCharacteristic(UUID uuid) throws Exception {
        if (uuid.equals(CHARACTERISTIC_MANUFACTURER_NAME_STRING)) {
            readManufacturer();
        } else if (uuid.equals(CHARACTERISTIC_MODEL_NUMBER_STRING)) {
            readModelNumber();
        } else if (uuid.equals(CHARACTERISTIC_SERIAL_NUMBER_STRING)) {
            readSerialNumber();
        } else if (uuid.equals(CHARACTERISTIC_HARDWARE_REVISION_STRING)) {
            readHardwareRevision();
        } else if (uuid.equals(CHARACTERISTIC_FIRMWARE_REVISION_STRING)) {
            readFirmwareRevision();
        } else if (uuid.equals(CHARACTERISTIC_SOFTWARE_REVISION_STRING)) {
            readSoftwareRevision();
        } else if (uuid.equals(CHARACTERISTIC_SYSTEM_ID)) {
            readSystemId();
        } else if (uuid.equals(CHARACTERISTIC_IEEE_11073)) {
            readIeee11073();
        } else if (uuid.equals(CHARACTERISTIC_PNP_ID)) {
            readPnpId();
        } else if (uuid.equals(SUOTA_VERSION_UUID)) {
            readSuotaVersion();
        } else if (uuid.equals(SUOTA_PATCH_DATA_CHAR_SIZE_UUID)) {
            readPatchDataSize();
        } else if (uuid.equals(SUOTA_MTU_UUID)) {
            readMtu();
        } else if (uuid.equals(SUOTA_L2CAP_PSM_UUID)) {
            readL2capPsm();
        }
    }

    private GattOperation getReadCharacteristicCommand(UUID uuid) {
        if (uuid.equals(CHARACTERISTIC_MANUFACTURER_NAME_STRING) && manufacturerNameCharacteristic != null) {
            return new GattOperation(manufacturerNameCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_MODEL_NUMBER_STRING) && modelNumberCharacteristic != null) {
            return new GattOperation(modelNumberCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_SERIAL_NUMBER_STRING) && serialNumberCharacteristic != null) {
            return new GattOperation(serialNumberCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_HARDWARE_REVISION_STRING) && hardwareRevisionCharacteristic != null) {
            return new GattOperation(hardwareRevisionCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_FIRMWARE_REVISION_STRING) && firmwareRevisionCharacteristic != null) {
            return new GattOperation(firmwareRevisionCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_SOFTWARE_REVISION_STRING) && softwareRevisionCharacteristic != null) {
            return new GattOperation(softwareRevisionCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_SYSTEM_ID) && systemIdCharacteristic != null) {
            return new GattOperation(systemIdCharacteristic);
        } else if (uuid.equals(CHARACTERISTIC_IEEE_11073) && ieee11073Characteristic != null) {
            return new GattOperation(ieee11073Characteristic);
        } else if (uuid.equals(CHARACTERISTIC_PNP_ID) && pnpIdCharacteristic != null) {
            return new GattOperation(pnpIdCharacteristic);
        }
        return null;
    }

    /**
     * Checks device information service availability
     * @param uuid UUID of the device info characteristic, null for the device info service
     */
    public boolean hasDeviceInfo(UUID uuid) {
        return deviceInfoService != null && (uuid == null || deviceInfoService.getCharacteristic(uuid) != null);
    }

    /**
     * Read the manufacturer characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readManufacturer() throws Exception {
        readSpecificCharacteristic(manufacturerNameCharacteristic);
    }

    /**
     * Read the model number characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readModelNumber() throws Exception {
        readSpecificCharacteristic(modelNumberCharacteristic);
    }

    /**
     * Read the serial number characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readSerialNumber() throws Exception {
        readSpecificCharacteristic(serialNumberCharacteristic);
    }

    /**
     * Read the hardware revision characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readHardwareRevision() throws Exception {
        readSpecificCharacteristic(hardwareRevisionCharacteristic);
    }

    /**
     * Read the firmware revision characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readFirmwareRevision() throws Exception {
        readSpecificCharacteristic(firmwareRevisionCharacteristic);
    }

    /**
     * Read the software revision characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readSoftwareRevision() throws Exception {
        readSpecificCharacteristic(softwareRevisionCharacteristic);
    }

    /**
     * Read the system id characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readSystemId() throws Exception {
        readSpecificCharacteristic(systemIdCharacteristic);
    }

    /**
     * Read the ieee11073 characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readIeee11073() throws Exception {
        readSpecificCharacteristic(ieee11073Characteristic);
    }

    /**
     * Read the pnp id characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readPnpId() throws Exception {
        readSpecificCharacteristic(pnpIdCharacteristic);
    }

    /**
     * Read the suota version characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readSuotaVersion() throws Exception {
        readSpecificCharacteristic(suotaVersionCharacteristic);
    }

    /**
     * Read the patch data size characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readPatchDataSize() throws Exception {
        readSpecificCharacteristic(patchDataSizeCharacteristic);
    }

    /**
     * Read the mtu characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readMtu() throws Exception {
        readSpecificCharacteristic(mtuCharacteristic);
    }

    /**
     * Read the l2cap characteristic
     * @throws Exception in case there is no connection with BLE device
     */
    public void readL2capPsm() throws Exception {
        readSpecificCharacteristic(l2capPsmCharacteristic);
    }

    /**
     * Queue read characteristic commands for reading the device's info. Either all available
     * or the specified device info to read on SuotaLibConfig class.
     *
     * @throws Exception if you try to read the device's info while not connected to device
     */
    public void readDeviceInfo() throws Exception {
        if (gatt == null || state != ManagerState.CONNECTED) {
            throw new Exception("Null Gatt Singleton. Make sure to connect before trying to read Device Info");
        }

        queueReadDeviceInfo();
    }
    // endregion

    // region Core Public Methods

    /**
     * Connects to bluetooth device
     */
    public void connect() {
        if (state != ManagerState.DISCONNECTED) {
            Log.e(TAG, "Cannot create new connection yet. The previous is still going!");
            return;
        }
        if (!SuotaUtil.checkConnectPermission(context)) {
            Log.e(TAG, "Missing BLUETOOTH_CONNECT permission");
            return;
        }
        reset();
        gattCallback = new GattCallback(this);
        state = ManagerState.CONNECTING;
        if (Build.VERSION.SDK_INT < 23) {
            gatt = device.connectGatt(context, false, gattCallback);
        } else {
            gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
        }
    }

    // region InitializeSuota Overloaded Methods
    // In order for suota to run properly this methods should be be called first, as well as setSuotaFile

    /**
     * Initializes Suota Protocol settings using default values
     * if Suota is already running this method does nothing
     *
     * @see #initializeSuota(int, int, int, int, int)
     * @see #initializeSuota(int, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int, int)
     */
    public void initializeSuota() {
        if (handleSuotaRunningOnNewConnectRequest())
            return;

        suotaProtocol = new SuotaProtocol(this);
        suotaFile.initBlocks(blockSize, chunkSize);
    }

    /**
     * Initializes Suota Protocol setting using the values passed as arguments
     * Use this when memory type is SPI
     * if Suota is already running this method does nothing
     *
     * @param blockSize selected block size
     * @param misoGpio selected miso gpio
     * @param mosiGpio selected mosi gpio
     * @param csGpio selected cs gpio
     * @param sckGpio selected sck gpio
     * @param imageBank selected image back
     *
     * @see #initializeSuota()
     * @see #initializeSuota(int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int, int)
     */
    public void initializeSuota(int blockSize, int misoGpio, int mosiGpio, int csGpio, int sckGpio, int imageBank) {
        if (handleSuotaRunningOnNewConnectRequest())
            return;

        suotaProtocol = new SuotaProtocol(this);
        this.blockSize = blockSize;
        suotaFile.initBlocks(blockSize, chunkSize);
        this.memoryType = MEMORY_TYPE_EXTERNAL_SPI;
        this.misoGpio = misoGpio;
        this.mosiGpio = mosiGpio;
        this.csGpio = csGpio;
        this.sckGpio = sckGpio;
        this.imageBank = imageBank;
    }

    /**
     * Initializes Suota Protocol setting using the values passed as arguments
     * Use this when memory type is I2C
     * if Suota is already running this method does nothing
     *
     * @param blockSize selected block size
     * @param i2cAddress selected i2c address
     * @param sclGpio selected scl gpio
     * @param sdaGpio selected sda gpio
     * @param imageBank selected image bank
     *
     * @see #initializeSuota()
     * @see #initializeSuota(int, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int, int)
     */
    public void initializeSuota(int blockSize, int i2cAddress, int sclGpio, int sdaGpio, int imageBank) {
        if (handleSuotaRunningOnNewConnectRequest())
            return;

        suotaProtocol = new SuotaProtocol(this);
        this.blockSize = blockSize;
        suotaFile.initBlocks(blockSize, chunkSize);
        this.memoryType = MEMORY_TYPE_EXTERNAL_I2C;
        this.i2cDeviceAddress = i2cAddress;
        this.sclGpio = sclGpio;
        this.sdaGpio = sdaGpio;
        this.imageBank = imageBank;
    }

    /**
     * Initializes Suota Protocol setting using the values passed as arguments
     * Use this when memory type is SPI
     * if Suota is already running this method does nothing
     *
     * @param firmware firmware to used for the update
     * @param blockSize selected block size
     * @param misoGpio selected miso gpio
     * @param mosiGpio selected mosi gpio
     * @param csGpio selected cs gpio
     * @param sckGpio selected sck gpio
     * @param imageBank selected image back
     *
     * @see #initializeSuota()
     * @see #initializeSuota(int, int, int, int, int)
     * @see #initializeSuota(int, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int)
     */
    public void initializeSuota(SuotaFile firmware, int blockSize, int misoGpio, int mosiGpio, int csGpio, int sckGpio, int imageBank) {
        setSuotaFile(firmware);
        initializeSuota(blockSize, misoGpio, mosiGpio, csGpio, sckGpio, imageBank);
    }

    /**
     * Initializes Suota Protocol setting using the values passed as arguments
     * Use this when memory type is I2C
     * if Suota is already running this method does nothing
     *
     * @param firmware firmware to used for the update
     * @param blockSize selected block size
     * @param i2cAddress selected i2c address
     * @param sclGpio selected scl gpio
     * @param sdaGpio selected sda gpio
     * @param imageBank selected image bank
     *
     * @see #initializeSuota()
     * @see #initializeSuota(int, int, int, int, int)
     * @see #initializeSuota(int, int, int, int, int, int)
     * @see #initializeSuota(SuotaFile, int, int, int, int, int, int)
     */
    public void initializeSuota(SuotaFile firmware, int blockSize, int i2cAddress, int sclGpio, int sdaGpio, int imageBank) {
        setSuotaFile(firmware);
        initializeSuota(blockSize, i2cAddress, sclGpio, sdaGpio, imageBank);
    }

    // endregion

    /**
     * This method actually starts SuotaProtocol
     * Make sure to call this only if you have successfully connected a bluetooth device first.
     * Otherwise this method does nothing
     */
    public void startUpdate() {
        if (gatt == null) {
            Log.e(TAG, "Null Gatt Singleton. Make sure to connect before trying to start update");
            failureNullGatt();
            return;
        }

        if (suotaProtocol == null)
            initializeSuota();
        if (suotaProtocol.isRunning()) {
            Log.e(TAG, "Previous Suota is still running");
            return;
        }

        if (blockSize != suotaFile.getBlockSize() || chunkSize != suotaFile.getChunkSize())
            suotaFile.initBlocks(blockSize, chunkSize);

        if (requestConnectionPriority) {
            Log.d(TAG, "Connection parameters update request (upload)");
            requestConnectionPriority(SuotaLibConfig.CONNECTION_PRIORITY_UPLOAD);
        }
        suotaProtocol.start();
    }

    /**
     * Disconnects from connected Bluetooth device
     */
    @SuppressLint("MissingPermission")
    public void disconnect() {
        if (state == ManagerState.DISCONNECTED)
            return;

        synchronized (gattQueueLock) {
            gattOperationPending = false;
            gattQueue.clear();
        }

        try {
            if (gatt == null)
                return;

            if (SuotaLibLog.MANAGER)
                Log.d(TAG, "Disconnecting from device...");
            gatt.disconnect();
            if (suotaProtocol != null)
                suotaProtocol.destroy();

            // handle case that trying to disconnect without having prior connected to device
            if (state == ManagerState.CONNECTING) {
                if (SuotaLibLog.MANAGER)
                    Log.d(TAG, "Closing Gatt...");
                gatt.close();
            }
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
    }

    /**
     * Cleans up everything !! first disconnect
     * Call this when you are done with SuotaManager
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void destroy() {
        Log.d(TAG, "Destroy");
        if (state != ManagerState.DISCONNECTED) {
            disconnect();
        }

        synchronized (gattQueueLock) {
            gattOperationPending = false;
            gattQueue.clear();
        }

        suotaProtocol = null;

        stopAsymmetricScanner();
    }

    public boolean supportsAsymmetricSuota() {
        return asymmetricSupport;
    }

    public boolean isAsymmetricSuotaPending() {
        return asymmetricPending;
    }

    public void startAsymmetricSuota() {
        if (asymmetricDataCharacteristic != null && asymmetricData == null) {
            Log.d(TAG, "Asymmetric SUOTA: Read reconnection data");
            enqueueGattOperation(new GattOperation(asymmetricDataCharacteristic));
            return;
        }
        Log.d(TAG, "Asymmetric SUOTA: Send reboot command");
        asymmetricPending = true;
        enqueueGattOperation(new GattOperation(asymmetricCharacteristic, new byte[] { (byte) SUOTA_ASYMMETRIC_REBOOT }));
    }

    // endregion

    // endregion

    // region Package Private Methods

    int getMemoryDevice() {
        return (memoryType << 24) | imageBank;
    }

    int getGpioMap() {
        switch (memoryType) {
            case MEMORY_TYPE_EXTERNAL_SPI:
                return getSpiGpioMap();
            case MEMORY_TYPE_EXTERNAL_I2C:
                return getI2cGpioMap();
            default:
                return 0;
        }
    }

    int getSpiGpioMap() {
        return (misoGpio << 24) | (mosiGpio << 16) | (csGpio << 8) | sckGpio;
    }

    int getI2cGpioMap() {
        return (i2cDeviceAddress << 16) | (sclGpio << 8) | sdaGpio;
    }

    public Context getContext() {
        return context;
    }

    SuotaManagerCallback getSuotaManagerCallback() {
        return suotaManagerCallback;
    }

    public void setUiContext(Context context) {
        uiContextWeakReference = new WeakReference<>(context);
    }

    WeakReference<Context> getUiContextWeakReference() {
        return uiContextWeakReference;
    }

    public boolean isGattOperationPending() {
        return gattOperationPending;
    }

    boolean isRebootSent() {
        return rebootSent;
    }

    BluetoothGatt getGatt() {
        return gatt;
    }

    void setGatt(BluetoothGatt gatt) {
        this.gatt = gatt;
    }

    void setState(ManagerState state) {
        this.state = state;
    }

    @SuppressLint("MissingPermission")
    void close() {
        Log.d(TAG, "Close");
        if (suotaProtocol != null) {
            suotaProtocol.destroy();
            suotaProtocol = null;
        }
        if (gatt == null)
            return;

        if (SuotaLibConfig.REFRESH_AFTER_UPDATE) {
            // Refresh device cache if update was successful
            if (refreshAfterUpdatePending) {
                refreshAfterUpdatePending = false;
                if (isRefreshRequired(device))
                    refreshDeviceCache(gatt);
            }
        }

        gatt.close();
        gatt = null;

        gattCallback.destroy();
        gattCallback = null;
    }

    void checkAsymmetric() {
        if (!asymmetricPending)
            return;
        if (asymmetricData != null && asymmetricData.length > 0) {
            Log.d(TAG, "Asymmetric SUOTA: Reconnect to device with data: " + SuotaUtil.hexArray(asymmetricData));
            MainThread.runOnUiThread(this::reconnectAsymmetricSuota);
        } else {
            Log.d(TAG, "Asymmetric SUOTA: Reconnect to device: " + device.getAddress());
            MainThread.runOnUiThread(SuotaLibConfig.ASYMMETRIC_SUOTA_RECONNECT_DELAY, this::connect);
        }
    }

    private void reconnectAsymmetricSuota() {
        asymmetricScanner = new SuotaScanner.Builder()
                .setContext(context)
                .setScanTimeout(SuotaLibConfig.ASYMMETRIC_SUOTA_RECONNECT_SCAN_DURATION)
                .setCheckPermissions(false)
                .setSkipLocationCheck(true)
                .setAllowDialogDisplay(false)
                .build();

        asymmetricScanner.scan(null, new ISuotaScanner() {
            boolean found;

            @Override
            public void onDeviceScan(BluetoothDevice bluetoothDevice, int rssi, byte[] scanRecord) {
                if (asymmetricScanner == null)
                    return;
                ByteBuffer buffer = ByteBuffer.wrap(scanRecord).order(ByteOrder.LITTLE_ENDIAN);
                while (buffer.remaining() > 2) {
                    int length = buffer.get() & 0xff;
                    if (length == 0)
                        break;
                    int type = buffer.get() & 0xff;
                    --length;
                    if (length > buffer.remaining())
                        break;
                    if (type == 0xff && length == 8) {
                        byte[] data = new byte[8];
                        buffer.get(data);
                        length -= 8;
                        if (Arrays.equals(data, asymmetricData)) {
                            found = true;
                            stopAsymmetricScanner();
                            device = bluetoothDevice;
                            Log.d(TAG, "Asymmetric SUOTA: Found device: " + device.getAddress());
                            connect();
                            break;
                        }
                    }
                    buffer.position(buffer.position() + length);
                }
            }

            @Override
            public void onScanStatusChange(ScanStatus newStatus) {
                if (newStatus == ScanStatus.STOPPED && !found) {
                    stopAsymmetricScanner();
                    notifyFailure(SuotaProfile.Errors.ASYMMETRIC_SCAN_FAILED);
                }
            }

            @Override
            public void onFailure(SuotaProfile.ScanFailure failure) {
                stopAsymmetricScanner();
                notifyFailure(SuotaProfile.Errors.ASYMMETRIC_SCAN_FAILED);
            }
        });
    }

    private void stopAsymmetricScanner() {
        SuotaScanner asymmetricScanner = this.asymmetricScanner;
        if (asymmetricScanner != null) {
            this.asymmetricScanner = null;
            asymmetricScanner.destroy();
        }
    }

    void enqueueGattOperation(GattOperation gattOperation) {
        synchronized (gattQueueLock) {
            if (gattOperationPending) {
                gattQueue.add(gattOperation);
            } else {
                executeGattOperation(gattOperation);
            }
        }
    }

    void enqueueGattOperations(List<GattOperation> gattOperations) {
        synchronized (gattQueueLock) {
            gattQueue.addAll(gattOperations);
            if (!gattOperationPending) {
                dequeueGattOperation();
            }
        }
    }

    void dequeueGattOperation() {
        synchronized (gattQueueLock) {
            gattOperationPending = false;
            if (gattQueue.isEmpty())
                return;
            executeGattOperation(gattQueue.poll());
        }
    }

    private void executeGattOperation(@NonNull GattOperation gattOperation) {
        if (gatt == null) {
            failureNullGatt();
            return;
        }

        if (gattOperation.execute(gatt)) {
            gattOperationPending = true;
            if (gattOperation.getType() == GattOperation.OperationType.REBOOT_COMMAND) {
                rebootSent = true;
                MainThread.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        suotaManagerCallback.onRebootSent();
                    }
                });
            }
        } else {
            notifyFailure(gattOperation.getType() != GattOperation.OperationType.MTU_REQUEST ? SuotaProfile.Errors.GATT_OPERATION_ERROR : SuotaProfile.Errors.MTU_REQUEST_FAILED);
        }
    }

    @SuppressLint("InlinedApi")
    void onSuotaProtocolSuccess() {
        refreshAfterUpdatePending = true;

        final double elapsedTime = suotaProtocol != null ? suotaProtocol.getElapsedTime() / 1000. : -1;
        final double uploadElapsedTime = suotaProtocol != null ? suotaProtocol.getUploadElapsedTime() / 1000. : -1;
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onSuccess(elapsedTime, uploadElapsedTime);
            }
        });

        if (requestConnectionPriority) {
            Log.d(TAG, "Connection parameters update request (normal)");
            requestConnectionPriority(SuotaLibConfig.CONNECTION_PRIORITY_NORMAL);
        }

        if (SuotaLibConfig.AUTO_REBOOT) {
            sendRebootCommand();
        } else if (SuotaLibConfig.ALLOW_DIALOG_DISPLAY) {
            showRebootPromptDialog();
        }

        // need to release wakelock
        if (suotaProtocol != null)
            suotaProtocol.destroy();
    }

    // endregion

    // region Gatt Callback Wrappers

    @SuppressLint("MissingPermission")
    void onServicesDiscovered(BluetoothGatt gatt) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onServicesDiscovered();
            }
        });

        if (SuotaLibConfig.REFRESH_ON_CONNECTION) {
            if (!refreshOnConnectionCalled) {
                refreshOnConnectionCalled = true;
                if (isRefreshRequired(device) && refreshDeviceCache(gatt)) {
                    Log.d(TAG, "Restart discovery after refresh");
                    gatt.discoverServices();
                    return;
                }
            }
        }

        initServices();

        if (SuotaLibConfig.ASYMMETRIC_SUOTA_AUTO_REBOOT && asymmetricSupport) {
            startAsymmetricSuota();
            return;
        }

        if (supportsSuota() || asymmetricSupport) {
            queueReadInfoOperations();
        } else {
            Log.e(TAG, "The device does not support SUOTA");
            notifyFailure(SuotaProfile.Errors.SUOTA_NOT_SUPPORTED);
        }
    }

    void onCharacteristicRead(@NonNull final BluetoothGattCharacteristic characteristic, byte[] value) {
        UUID uuid = characteristic.getUuid();
        if (suotaInfoUuids.contains(uuid)) {
            onSuotaInfoRead(characteristic, value);
        } else if (deviceInfoUuids.contains(uuid)) {
            onDeviceInfoRead(characteristic, value);
        } else if (uuid.equals(SUOTA_ASYMMETRIC_DATA_CHARACTERISTIC_UUID)) {
            asymmetricData = value;
            startAsymmetricSuota();
        } else {
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onCharacteristicRead(SuotaProfile.CharacteristicGroup.OTHER, characteristic, value);
                }
            });
        }
    }

    void onCharacteristicWrite(@NonNull BluetoothGattCharacteristic characteristic) {
        if (suotaProtocol != null)
            suotaProtocol.onCharacteristicWrite(characteristic);
    }

    void onCharacteristicChanged(@NonNull BluetoothGattCharacteristic characteristic, byte[] value) {
        if (suotaProtocol != null)
            suotaProtocol.onCharacteristicChanged(characteristic, value);
    }

    void onDescriptorWrite(@NonNull BluetoothGattDescriptor descriptor) {
        if (suotaProtocol != null)
            suotaProtocol.onDescriptorWrite(descriptor);
    }

    void onMtuChanged(int mtu) {
        if (this.mtu == mtu)
            return;
        this.mtu = mtu;
        updateChunkSize();
        xiaomiMtuIssueWorkaround();
    }

    void failureNullGatt() {
        notifyFailure(SuotaProfile.Errors.NULL_GATT_SINGLETON);
    }

    // endregion

    // region Private Methods

    private void readSpecificCharacteristic(BluetoothGattCharacteristic characteristic) throws Exception {
        if (gatt == null || state != ManagerState.CONNECTED) {
            throw new Exception("Null Gatt Singleton. Make sure to connect before trying to read Device Info");
        }

        if (characteristic == null) {
            Log.e(TAG, "Characteristic not available");
            return;
        }

        enqueueGattOperation(new GattOperation(characteristic));
    }

    private void initServices() {
        suotaService = gatt.getService(SUOTA_SERVICE_UUID);
        if (suotaService != null) {
            Log.d(TAG, "Found SUOTA service");
            memDevCharacteristic = suotaService.getCharacteristic(SUOTA_MEM_DEV_UUID);
            gpioMapCharacteristic = suotaService.getCharacteristic(SUOTA_GPIO_MAP_UUID);
            memoryInfoCharacteristic = suotaService.getCharacteristic(SUOTA_MEM_INFO_UUID);
            patchLengthCharacteristic = suotaService.getCharacteristic(SUOTA_PATCH_LEN_UUID);
            patchDataCharacteristic = suotaService.getCharacteristic(SUOTA_PATCH_DATA_UUID);
            if (patchDataCharacteristic != null)
                patchDataCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
            serviceStatusCharacteristic = suotaService.getCharacteristic(SUOTA_SERV_STATUS_UUID);
            if (serviceStatusCharacteristic != null)
                serviceStatusClientConfigDescriptor = serviceStatusCharacteristic.getDescriptor(CLIENT_CONFIG_DESCRIPTOR);

            suotaVersionCharacteristic = suotaService.getCharacteristic(SUOTA_VERSION_UUID);
            patchDataSizeCharacteristic = suotaService.getCharacteristic(SUOTA_PATCH_DATA_CHAR_SIZE_UUID);
            mtuCharacteristic = suotaService.getCharacteristic(SUOTA_MTU_UUID);
            l2capPsmCharacteristic = suotaService.getCharacteristic(SUOTA_L2CAP_PSM_UUID);
            if (suotaVersionCharacteristic != null)
                Log.d(TAG, "Found SUOTA version characteristic");
            if (patchDataSizeCharacteristic != null)
                Log.d(TAG, "Found SUOTA patch data char size characteristic");
            if (mtuCharacteristic != null)
                Log.d(TAG, "Found SUOTA MTU characteristic");
            if (l2capPsmCharacteristic != null)
                Log.d(TAG, "Found SUOTA L2CAP PSM characteristic");
        } else {
            asymmetricService = gatt.getService(SUOTA_ASYMMETRIC_SERVICE_UUID);
            if (asymmetricService != null) {
                asymmetricCharacteristic = asymmetricService.getCharacteristic(SUOTA_ASYMMETRIC_CHARACTERISTIC_UUID);
                asymmetricDataCharacteristic = asymmetricService.getCharacteristic(SUOTA_ASYMMETRIC_DATA_CHARACTERISTIC_UUID);
                if (asymmetricCharacteristic != null) {
                    Log.d(TAG, "Found asymmetric SUOTA service");
                    asymmetricSupport = true;
                }
            }
        }

        deviceInfoService = gatt.getService(SERVICE_DEVICE_INFORMATION);
        if (deviceInfoService != null) {
            manufacturerNameCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_MANUFACTURER_NAME_STRING);
            modelNumberCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_MODEL_NUMBER_STRING);
            serialNumberCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_SERIAL_NUMBER_STRING);
            hardwareRevisionCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_HARDWARE_REVISION_STRING);
            firmwareRevisionCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_FIRMWARE_REVISION_STRING);
            softwareRevisionCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_SOFTWARE_REVISION_STRING);
            systemIdCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_SYSTEM_ID);
            ieee11073Characteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_IEEE_11073);
            pnpIdCharacteristic = deviceInfoService.getCharacteristic(CHARACTERISTIC_PNP_ID);
        }
    }

    @SuppressLint("MissingPermission")
    private void requestConnectionPriority(int connectionPriority) {
        if (gatt == null) {
            failureNullGatt();
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Log.d(TAG, "Request connection priority: " + connectionPriority);
            gatt.requestConnectionPriority(connectionPriority);
        }
    }

    private void reset() {
        synchronized (gattQueueLock) {
            gattOperationPending = false;
            gattQueue.clear();
        }

        synchronized (suotaInfoMapLock) {
            suotaInfoMap = new HashMap<>();
            totalSuotaInfo = 0;
        }

        synchronized (deviceInfoMapLock) {
            deviceInfoMap = new HashMap<>();
            totalDeviceInfo = 0;
        }

        suotaService = null;
        memDevCharacteristic = null;
        gpioMapCharacteristic = null;
        memoryInfoCharacteristic = null;
        patchLengthCharacteristic = null;
        patchDataCharacteristic = null;
        serviceStatusCharacteristic = null;
        serviceStatusClientConfigDescriptor = null;
        suotaVersionCharacteristic = null;
        patchDataSizeCharacteristic = null;
        mtuCharacteristic = null;
        l2capPsmCharacteristic = null;

        deviceInfoService = null;
        manufacturerNameCharacteristic = null;
        modelNumberCharacteristic = null;
        serialNumberCharacteristic = null;
        hardwareRevisionCharacteristic = null;
        firmwareRevisionCharacteristic = null;
        softwareRevisionCharacteristic = null;
        systemIdCharacteristic = null;
        ieee11073Characteristic = null;
        pnpIdCharacteristic = null;
        asymmetricService = null;
        asymmetricCharacteristic = null;
        asymmetricDataCharacteristic = null;

        suotaVersion = -1;
        mtu = SuotaProfile.DEFAULT_MTU;
        patchDataSize = Default.CHUNK_SIZE;
        chunkSize = Default.CHUNK_SIZE;
        l2capPsm = -1;
        readSuotaVersion = false;
        readPatchDataSize = false;
        readMtu = false;
        readL2capPsm = false;
        asymmetricSupport = false;
        asymmetricPending = false;
        asymmetricData = null;
        asymmetricScanner = null;

        manufacturer = null;
        modelNumber = null;
        serialNumber = null;
        hardwareRevision = null;
        firmwareRevision = null;
        softwareRevision = null;
        systemId = null;
        ieee11073 = null;
        pnpId = null;

        refreshOnConnectionCalled = false;
        refreshAfterUpdatePending = false;
        rebootSent = false;
        isDeviceInfoReadGroupPending = new AtomicBoolean();
        isSuotaInfoReadGroupPending = false;
    }

    private boolean handleSuotaRunningOnNewConnectRequest() {
        if (suotaProtocol != null && suotaProtocol.isRunning()) {
            Log.e(TAG, "Previous Suota was still running...");
            return true;
        } else if (suotaProtocol != null) {
            if (SuotaLibLog.MANAGER)
                Log.d(TAG, "Destroying old suota protocol object");
            suotaProtocol.destroy();
        }

        return false;
    }

    private void notifyFailure(final int value) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onFailure(value);
            }
        });
    }

    private void onDeviceInfoRead(final BluetoothGattCharacteristic characteristic, byte[] value) {
        if (SuotaLibConfig.NOTIFY_DEVICE_INFO_READ) {
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onCharacteristicRead(SuotaProfile.CharacteristicGroup.DEVICE_INFO, characteristic, value);
                }
            });
        }

        assignDeviceInfo(characteristic, value);

        if (!isDeviceInfoReadGroupPending.get())
            return;

        int totalInfo;
        int infoReadTillNow;
        synchronized (deviceInfoMapLock) {
            deviceInfoMap.put(characteristic.getUuid(), characteristic);
            totalInfo = totalDeviceInfo;
            infoReadTillNow = deviceInfoMap.size();
        }

        if (totalInfo == infoReadTillNow) {
            isDeviceInfoReadGroupPending.set(false);
            if (SuotaLibConfig.NOTIFY_DEVICE_INFO_READ_COMPLETE) {
                MainThread.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        suotaManagerCallback.onDeviceInfoReadCompleted(SuotaProfile.DeviceInfoReadStatus.SUCCESS);
                    }
                });
            }
        }
    }

    private void assignDeviceInfo(@NonNull BluetoothGattCharacteristic characteristic, byte[] value) {
        UUID uuid = characteristic.getUuid();
        if (uuid.equals(CHARACTERISTIC_MANUFACTURER_NAME_STRING)) {
            manufacturer = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_MODEL_NUMBER_STRING)) {
            modelNumber = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_SERIAL_NUMBER_STRING)) {
            serialNumber = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_HARDWARE_REVISION_STRING)) {
            hardwareRevision = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_FIRMWARE_REVISION_STRING)) {
            firmwareRevision = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_SOFTWARE_REVISION_STRING)) {
            softwareRevision = new String(value, StandardCharsets.UTF_8);
        } else if (uuid.equals(CHARACTERISTIC_SYSTEM_ID)) {
            systemId = value;
        } else if (uuid.equals(CHARACTERISTIC_IEEE_11073)) {
            ieee11073 = value;
        } else if (uuid.equals(CHARACTERISTIC_PNP_ID)) {
            pnpId = value;
        }
    }

    private void onSuotaInfoRead(final BluetoothGattCharacteristic characteristic, byte[] value) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onCharacteristicRead(SuotaProfile.CharacteristicGroup.SUOTA_INFO, characteristic, value);
            }
        });

        onSuotaReadUpdate(characteristic, value);

        if (!isSuotaInfoReadGroupPending)
            return;

        int totalInfo;
        int infoReadTillNow;
        synchronized (suotaInfoMapLock) {
            suotaInfoMap.put(characteristic.getUuid(), characteristic);
            totalInfo = totalSuotaInfo;
            infoReadTillNow = suotaInfoMap.size();
        }

        if (totalInfo == infoReadTillNow) {
            isSuotaInfoReadGroupPending = false;
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onDeviceReady();
                }
            });

            sendMtuRequest();
        }
    }

    private void onSuotaReadUpdate(@NonNull BluetoothGattCharacteristic characteristic, byte[] value) {
        UUID uuid = characteristic.getUuid();

        if (uuid.equals(SUOTA_VERSION_UUID)) {
            readSuotaVersion = true;
            if (value.length < 1) {
                Log.e(TAG, "Invalid SUOTA version value");
                return;
            }
            suotaVersion = value[0] & 0xff;
            Log.d(TAG, "SUOTA version: " + suotaVersion);
        } else if (uuid.equals(SUOTA_PATCH_DATA_CHAR_SIZE_UUID)) {
            readPatchDataSize = true;
            if (value.length < 2) {
                Log.e(TAG, "Invalid patch data size value");
                return;
            }
            patchDataSize = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
            Log.d(TAG, "Patch data size: " + patchDataSize);
            updateChunkSize();
        } else if (uuid.equals(SUOTA_MTU_UUID)) {
            readMtu = true;
            if (value.length < 2) {
                Log.e(TAG, "Invalid MTU value");
                return;
            }
            mtu = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
            Log.d(TAG, "MTU: " + mtu);
            updateChunkSize();
        } else if (uuid.equals(SUOTA_L2CAP_PSM_UUID)) {
            readL2capPsm = true;
            if (value.length < 2) {
                Log.e(TAG, "Invalid L2CAP PSM value");
                return;
            }
            l2capPsm = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
            Log.d(TAG, "L2CAP PSM: " + l2capPsm);
        }
    }

    private void showRebootPromptDialog() {
        if (uiContextWeakReference == null)
            return;
        final Context context = uiContextWeakReference.get();
        if (context == null)
            return;

        rebootDialog = new DialogFragmentBuilder()
                .setType(DialogFragmentBuilder.REBOOT_DIALOG)
                .setTitle(context.getString(R.string.suota_upload_completed_title))
                .setMessage(context.getString(R.string.suota_reboot_msg))
                .setNegative(context.getString(android.R.string.no))
                .setCancelable(false)
                .setCancelableOnTouchOutside(false)
                .buildV4();

        rebootDialog.setOnPositiveClickListener(new DialogFragmentManager.OnResponseListener() {
            @Override
            public void onActionPerformed() {
                sendRebootCommand();
            }
        });

        DialogFragmentManager.OnResponseListener onRebootDenied = new DialogFragmentManager.OnResponseListener() {
            @Override
            public void onActionPerformed() {
                if (SuotaLibConfig.AUTO_DISCONNECT_IF_REBOOT_DENIED) {
                    disconnect();
                }
            }
        };
        rebootDialog.setOnNegativeClickListener(onRebootDenied);

        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (uiContextWeakReference == null)
                    return;
                final Context context = uiContextWeakReference.get();
                if (context == null)
                    return;
                try {
                    rebootDialog.showDialog(((AppCompatActivity)context).getSupportFragmentManager());
                } catch (Exception e) {
                    // in case the activity isn't visible trying to show the dialog fragment will cause the app to crash
                    // thus we catch the exception and let the library's user know there is a pending dialog
                    suotaManagerCallback.pendingRebootDialog(rebootDialog);
                }
            }
        });
    }

    private boolean supportsSuota() {
        return suotaService != null
                && memDevCharacteristic != null
                && gpioMapCharacteristic != null
                && memoryInfoCharacteristic != null
                && patchLengthCharacteristic != null
                && patchDataCharacteristic != null
                && serviceStatusCharacteristic != null
                && serviceStatusClientConfigDescriptor != null;
    }

    private void updateChunkSize() {
        chunkSize = Math.min(patchDataSize, mtu - 3);
        if (SuotaLibLog.MANAGER)
            Log.d(TAG,  "Chunk size set to " + chunkSize);
    }

    @SuppressLint("MissingPermission")
    private static boolean isRefreshRequired(BluetoothDevice device) {
        if (SuotaLibConfig.REFRESH_REQUIRED)
            return true;
        if (!SuotaLibConfig.REFRESH_REQUIRED_IF_BONDED)
            return false;
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        return bluetoothAdapter != null && bluetoothAdapter.getBondedDevices().contains(device);
    }

    private static boolean refreshDeviceCache(@NonNull BluetoothGatt gatt) {
        Log.d(TAG, "Refresh device cache");
        try {
            Method refreshMethod = gatt.getClass().getMethod("refresh", (Class[]) null);
            if (refreshMethod == null)
                return false;
            boolean result = (Boolean) refreshMethod.invoke(gatt, (Object[]) null);
            if (!result)
                Log.d(TAG, "Refresh failed");
            return result;
        } catch (Exception e) {
            Log.e(TAG, "An exception occurred while refreshing device cache", e);
        }
        return false;
    }

    private void queueReadInfoOperations() {
        if (SuotaLibConfig.AUTO_READ_DEVICE_INFO && SuotaLibConfig.READ_DEVICE_INFO_FIRST)
            queueReadDeviceInfo();

        queueReadSuotaInfo();

        if (SuotaLibConfig.AUTO_READ_DEVICE_INFO && !SuotaLibConfig.READ_DEVICE_INFO_FIRST)
            queueReadDeviceInfo();
    }

    private List<GattOperation> getDeviceInfoReadOperations() {
        List<GattOperation> gattOperations = new ArrayList<>();
        if (SuotaLibConfig.READ_ALL_DEVICE_INFO) {
            if (manufacturerNameCharacteristic != null)
                gattOperations.add(new GattOperation(manufacturerNameCharacteristic));
            if (modelNumberCharacteristic != null)
                gattOperations.add(new GattOperation(modelNumberCharacteristic));
            if (serialNumberCharacteristic != null)
                gattOperations.add(new GattOperation(serialNumberCharacteristic));
            if (hardwareRevisionCharacteristic != null)
                gattOperations.add(new GattOperation(hardwareRevisionCharacteristic));
            if (firmwareRevisionCharacteristic != null)
                gattOperations.add(new GattOperation(firmwareRevisionCharacteristic));
            if (softwareRevisionCharacteristic != null)
                gattOperations.add(new GattOperation(softwareRevisionCharacteristic));
            if (systemIdCharacteristic != null)
                gattOperations.add(new GattOperation(systemIdCharacteristic));
            if (ieee11073Characteristic != null)
                gattOperations.add(new GattOperation(ieee11073Characteristic));
            if (pnpIdCharacteristic != null)
                gattOperations.add(new GattOperation(pnpIdCharacteristic));
        } else {
            for (UUID uuid : SuotaLibConfig.DEVICE_INFO_TO_READ) {
                GattOperation operation = getReadCharacteristicCommand(uuid);
                if (operation != null)
                    gattOperations.add(operation);
            }
        }
        return gattOperations;
    }

    private void queueReadDeviceInfo() {
        if (gatt == null) {
            failureNullGatt();
            return;
        }

        if (isDeviceInfoReadGroupPending.getAndSet(true)) {
            if (SuotaLibLog.MANAGER)
                Log.d(TAG, "Another device info read operation is pending");
            return;
        }

        if (deviceInfoService == null) {
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onDeviceInfoReadCompleted(SuotaProfile.DeviceInfoReadStatus.NO_DEVICE_INFO);
                }
            });
            return;
        }

        List<GattOperation> operationsToQueue = getDeviceInfoReadOperations();

        // If no device info available to read, trigger the onInfoReadCallback now
        if (operationsToQueue.size() == 0) {
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onDeviceInfoReadCompleted(SuotaProfile.DeviceInfoReadStatus.NO_DEVICE_INFO);
                }
            });
            return;
        }

        synchronized (deviceInfoMapLock) {
            totalDeviceInfo += operationsToQueue.size();
        }

        enqueueGattOperations(operationsToQueue);
    }

    private List<GattOperation> getSuotaInfoReadOperations() {
        List<GattOperation> gattOperations = new ArrayList<>();
        if (suotaVersionCharacteristic != null)
            gattOperations.add(new GattOperation(suotaVersionCharacteristic));
        if (patchDataSizeCharacteristic != null)
            gattOperations.add(new GattOperation(patchDataSizeCharacteristic));
        if (mtuCharacteristic != null)
            gattOperations.add(new GattOperation(mtuCharacteristic));
        if (l2capPsmCharacteristic != null)
            gattOperations.add(new GattOperation(l2capPsmCharacteristic));
        return gattOperations;
    }

    private void queueReadSuotaInfo() {
        if (gatt == null) {
            failureNullGatt();
            return;
        }

        isSuotaInfoReadGroupPending = true;

        List<GattOperation> operationToQueue = getSuotaInfoReadOperations();

        // If no device info available to read, trigger the onInfoReadCallback now
        if (operationToQueue.size() == 0) {
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.onDeviceReady();
                }
            });
            return;
        }

        synchronized (suotaInfoMapLock) {
            totalSuotaInfo += operationToQueue.size();
        }

        enqueueGattOperations(operationToQueue);
    }

    private void sendMtuRequest() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || mtu != SuotaProfile.DEFAULT_MTU || mtu >= patchDataSize + 3)
            return;

        if (SuotaLibLog.MANAGER)
            Log.d(TAG, "Send MTU request");

        enqueueGattOperation(new GattOperation(patchDataSize + 3));
    }

    private void xiaomiMtuIssueWorkaround() {
        if (!Build.MANUFACTURER.equals("Xiaomi") || !(new File("/system/lib/libbtsession.so").exists()))
            return;
        if (mtuCharacteristic != null) {
            Log.d(TAG, "Workaround for Xiaomi MTU issue. Read MTU again.");
            enqueueGattOperation(new GattOperation(mtuCharacteristic));
        }
    }

    private void sendRebootCommand() {
        if (gatt == null) {
            failureNullGatt();
            return;
        }

        if (SuotaLibLog.MANAGER)
            Log.d(TAG, "Send SUOTA reboot command");

        enqueueGattOperation(new GattOperation(GattOperation.OperationType.REBOOT_COMMAND, memDevCharacteristic, SuotaProfile.Constants.SUOTA_REBOOT));
    }

    // endregion
}
